/**
    MVP Tree data structure using graph database for I/O storage
    Copyright (C) 2014  David G. Starkweather

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    David Starkweather starkd88@gmail.com
**/

package org.phash.mvptree;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Relationship;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.graphdb.index.IndexManager;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;

/**
 * Nodefactory class for creating nodes for the MVPTree,
 * within context of graph db database.
 * Both tree nodes and datapoint nodes.
 * This class can be subclassed to represent a datatype
 * to be saved.
 **/
public abstract class NodeFactory {

    protected static final String BRANCHFACTOR    = "BRANCHFACTOR";
    protected static final String PATHLENGTH      = "PATHLENGTH";
    protected static final String LEAFCAPACITY    = "LEAFCAPACITY";
    protected static final String DATAPOINTINDEX  = "DATAPOINTS";
    protected static final String POINTNAME       = "POINTNAME";
    protected static final String LEVELSPERNODE   = "NLEVELSPERNODE";
   
    protected Integer branchFactor, pathLength, leafCapacity, nLevels;
    
    protected final GraphDatabaseService graphdb;

    private static void registerShutdownHook(final GraphDatabaseService graphdb){
	
	Runtime.getRuntime().addShutdownHook(new Thread() {
		@Override
		    public void run(){
		    graphdb.shutdown();
		}
	    });
    }

    /** constructor
     * @param storeDir String dir location of graph database
     * @param bf       branch factor parameter - e.g. 2
     * @param pl       pathlength parameter - e.g. 5
     * @param lc       leaf capacity parameter - e.g. 30
     * @param nl       number levels in node (number vantage points) - e.g. 2
     **/
    public NodeFactory(String storeDir, int bf, int pl, int lc, int nl){

	graphdb = new EmbeddedGraphDatabase(storeDir);
	registerShutdownHook(graphdb);


	Node refNode = graphdb.getReferenceNode();

	if (refNode.hasRelationship(MVPRelationshipTypes.TO_TOP , Direction.OUTGOING)){
	    Relationship toprel = refNode.getSingleRelationship(MVPRelationshipTypes.TO_TOP, Direction.OUTGOING);
	    this.branchFactor   = (int)toprel.getProperty(BRANCHFACTOR, bf);
	    this.pathLength     = (int)toprel.getProperty(PATHLENGTH, pl);
	    this.leafCapacity   = (int)toprel.getProperty(LEAFCAPACITY, lc);
	    this.nLevels        = (int)toprel.getProperty(LEVELSPERNODE, nLevels);
	} else {
	    this.branchFactor = bf;
	    this.pathLength = pl;
	    this.leafCapacity = lc;
	    this.nLevels = nl;
	    saveParameters();
	}
    }


    protected int getBF(){ return branchFactor; }

    protected int getPL(){ return pathLength; }

    protected int getLC(){ return leafCapacity; }

    protected int getNL(){ return nLevels; }

    protected void shutdown(){
	saveParameters();
	graphdb.shutdown();
    }

    private void saveParameters() {
	Transaction tx = graphdb.beginTx();
	try {
	    Node refNode = graphdb.getReferenceNode();
	    if (refNode.hasRelationship(MVPRelationshipTypes.TO_TOP, Direction.OUTGOING)){
		Relationship rel = refNode.getSingleRelationship(MVPRelationshipTypes.TO_TOP, Direction.OUTGOING);
		rel.setProperty(BRANCHFACTOR, branchFactor);
		rel.setProperty(PATHLENGTH, pathLength);
		rel.setProperty(LEAFCAPACITY, leafCapacity);
		rel.setProperty(LEVELSPERNODE, nLevels);
	    }
	    tx.success();
	}finally {
	    tx.finish();
	}
    }

    protected MVPNode getTopNode() {
	Node node = graphdb.getReferenceNode();
	Relationship rel = node.getSingleRelationship(MVPRelationshipTypes.TO_TOP, Direction.OUTGOING);
	if (rel != null){
	    Node endNode = rel.getEndNode();
	    return NodeFactory.wrapNode(endNode);
	}
	return null;
    }

    protected static MVPNode wrapNode(Node node) {
	if (node != null){
	    int ntype = (int)node.getProperty(MVPNode.NODETYPE);
	    if (ntype == NodeType.INTERNAL_NODE.ordinal()) {
		return new MVPInternal(node);
	    } else if (ntype == NodeType.LEAF_NODE.ordinal()){
		return new MVPLeaf(node);
	    }
	}
	return null;
    }


    private Node getReferenceNode() {

	return graphdb.getReferenceNode();

    }

    protected MVPInternal createInternalNode() {
	Node newNode = null;
	Transaction tx = graphdb.beginTx();
	try {
	    newNode = graphdb.createNode();
	    tx.success();
	} finally {
	    tx.finish();
	}

	return new MVPInternal(newNode);
    }

    protected MVPLeaf createLeafNode() {
	Node newNode;
	Transaction tx = graphdb.beginTx();
	try {
	    newNode = graphdb.createNode();
	    tx.success();
	}finally {
	    tx.finish();
	}
	return new MVPLeaf(newNode);
    }

    protected void indexPoints(Iterable<Datapoint> points) {
	IndexManager index = graphdb.index();
	Index<Node> nodeIndex = index.forNodes(DATAPOINTINDEX);
	
	for (Datapoint pnt : points){
	    /* add each point to the index */
	    Transaction tx = graphdb.beginTx();
	    try {
		nodeIndex.add(pnt.getNode(), POINTNAME, pnt.getId());
		pnt.setActive(true);
		tx.success();
	    }finally {
		tx.finish();
	    }
	}
    }

    protected void clearDatapointIndex(){
	IndexManager index = graphdb.index();
	Index<Node> nodeIndex = index.forNodes(DATAPOINTINDEX);
	Transaction tx = graphdb.beginTx();
	try {
	    nodeIndex.delete();
	    tx.success();
	} finally {
	    tx.finish();
	}
    }

    protected int getPointCount(){
	Index<Node> nodeIndex = graphdb.index().forNodes(DATAPOINTINDEX);
	IndexHits<Node> allpoints = nodeIndex.query(POINTNAME, "*");
	return allpoints.size();
    }


    /**
     * create a datapoint object
     * @return Datapoint created object
     *
     **/
    public abstract Datapoint createDatapoint();

    
    protected abstract Datapoint createDatapoint(Node node);
    /**
     * lookup a datapoint by its string id.
     *
     * @param id String identifying the datapoint.
     *
     * @return Datapoint found point.
     **/
    public abstract Datapoint lookupPoint(String id);

    /** delete a datapoint from graphdatabase.
     * @param id String identifying the datapoint.
     *
     * @return void
     **/
    public abstract void deletePoint(String id);

}